module AboutYou
  module SDK
    ###
    # The Query class coordinates the building, executing and parsing of one or multiple API-calls
    #
    # Author:: Collins GmbH & Co KG
    ###
    class Query
      # QueryBuilder is a helper class containing most of the logix behind
      # building the api-request
      include AboutYou::SDK::QueryBuilder

      # used for checking whether a query contains a category-request or not
      QUERY_TREE   = 'category_tree'
      # used for checking whether a query contains a facets-request or not
      QUERY_FACETS = 'facets'

      # The Api-Client which performs the Api-Calls
      attr_accessor :client
      # the query built by the app itself
      attr_accessor :query
      # a Hash containing the mapping for api_call_name => method_name_in_model_factory
      attr_accessor :mapping
      # the model factory which builds the models from the api-response
      attr_accessor :factory
      # a helper-query used for fetching categories and/or facets if needed
      attr_accessor :ghost_query
      # the actual query send to the api containing query and ghost-query
      attr_accessor :all_query

      ###
      # the Constructor for the Query class
      #
      # * *Args*    :
      #   - +client+ -> an instance of AboutYou::SDK::Client
      #   - +factory+ -> an instance of AboutYou::SDK::Factory::DefaultModelFactory
      #
      # * *Returns* :
      #   - Instance of AboutYou::SDK::Query
      ###
      def initialize(client, factory)
        self.client = client
        self.query = []
        self.ghost_query = []
        self.all_query = []
        self.factory = factory
        self.mapping = {
          'autocompletion' => 'create_autocomplete',
          'basket'         => 'create_basket',
          'category'       => 'create_categories_result',
          'category_tree'  => 'create_category_tree',
          'facets'         => 'create_facets_list',
          'facet'          => 'create_facet_list',
          'facet_types'    => 'create_facet_types',
          'products'       => 'create_products_result',
          'products_eans'  => 'create_products_ean_result',
          'product_search' => 'create_product_search_result',
          'suggest'        => 'create_suggest',
          'get_order'      => 'create_order',
          'initiate_order' => 'initiate_order',
          'child_apps'     => 'create_child_apps',
          'live_variant'   => 'create_variants_result',
          'did_you_mean'   => 'create_spell_correction'
        }
      end

      ###
      # wrapper-method for fetch_autocomplete which coordinates Category- and Facet-Manager
      #
      # * *Args*    :
      #   - +searchword+ -> a String containing the word to search completitions for
      #   - +limit+ -> Maximum number of results [optional]
      #   - +types+ -> Array of types to search for [optional]
      #
      # * *Returns* :
      #   - Instance of AboutYou::SDK::Query
      ###
      def fetch_autocomplete(searchword, limit = nil, types = nil)
        super(searchword, limit, types)

        require_category_tree
        require_facets

        self
      end

      ###
      # wrapper-method for fetch_basket which coordinates Category- and Facet-Manager
      #
      # * *Args*    :
      #   - +session_id+ -> a String containing the session id of an user
      #
      # * *Returns* :
      #   - Instance of AboutYou::SDK::Query
      ###
      def fetch_basket(session_id)
        super(session_id)

        require_category_tree
        require_facets

        self
      end

      def fetch_facets(group_ids = [])
        super(group_ids)

        self
      end

      ###
      # wrapper-method for fetch_products_by_ids which coordinates Category- and Facet-Manager
      #
      # * *Args*    :
      #   - +ids+ -> Either a single id or an Array of ids which should be fetched
      #   - +fields+ -> Additional product fields which should be fetched for each product [optional]
      #
      # * *Returns* :
      #   - Instance of AboutYou::SDK::Query
      ###
      def fetch_products_by_ids(ids, fields = [])
        super(ids, fields)

        require_category_tree if
        AboutYou::SDK::Criteria::ProductFields.requires_categories(fields)
        require_facets if
        AboutYou::SDK::Criteria::ProductFields.requires_facets(fields)

        self
      end

      ###
      # wrapper-method for fetch_products_by_eans which coordinates Category- and Facet-Manager
      #
      # * *Args*    :
      #   - +eans+ -> Either a single ean or an Array of eans which should be fetched
      #   - +fields+ -> Additional product fields which should be fetched for each product [optional]
      #
      # * *Returns* :
      #   - Instance of AboutYou::SDK::Query
      ###
      def fetch_products_by_eans(eans, fields = [])
        super(eans, fields)

        require_category_tree if
        AboutYou::SDK::Criteria::ProductFields.requires_categories(fields)
        require_facets if
        AboutYou::SDK::Criteria::ProductFields.requires_facets(fields)

        self
      end

      ###
      # wrapper-method for fetch_product_search which coordinates Category- and Facet-Manager
      #
      # * *Args*    :
      #   - +criteria+ -> Hash containing one or multiple search terms
      #
      # * *Returns* :
      #   - Instance of AboutYou::SDK::Query
      ###
      def fetch_product_search(criteria)
        super(criteria)

        require_category_tree if criteria.requires_categories
        require_facets if criteria.requires_facets

        self
      end

      ###
      # this method checks whether it is neccessary for the query to get the category-tree from the api
      #
      # * *Args*    :
      #   - +fetchForced+ -> determines whether the requirance of the category tree is forced
      #
      # * *Returns* :
      #   - Instance of AboutYou::SDK::Query
      ###
      def require_category_tree(fetch_forced = false)
        return self unless

        fetch_forced || factory.category_manager.empty?

        ghost_query.push(
          QUERY_TREE => {
            'category_tree' => {
              'version' => '2'
            }
          }
        )

        self
      end

      ###
      # this method checks whether it is neccessary for the query to get the facets from the api
      #
      # * *Args*    :
      #   - +fetchForced+ -> determines whether the requirance of the facets is forced
      #
      # * *Returns* :
      #   - Instance of AboutYou::SDK::Query
      ###
      def require_facets(fetch_forced = false)
        return self unless fetch_forced || factory.facet_manager.empty?

        ghost_query.push(
          QUERY_FACETS => {
            'facets' => {}
          }
        )

        self
      end

      ###
      # the methods builds the complete query string which represents the body of the api call
      #
      # * *Returns* :
      #   - JSON-String containing all queries which need to be executed
      ###
      def query_string
        result = []

        ghost_query.each do |ghost_query|
          result.push(
            ghost_query[ghost_query.keys[0]]
          ) if ghost_query.is_a?(Hash)
        end

        self.all_query = result + query

        (result + query).to_json
      end

      ###
      # requests all of the queries and returns the parsed api-response
      #
      # * *Returns* :
      #   - an Array containing all of the models build with the data of the api
      ###
      def execute
        return [] if query.empty? && ghost_query.empty?
        parse_result(client.request(query_string), query.count > 1)
      end

      ###
      # requests all of the queries and returns only the first parsed api-response
      #
      # * *Returns* :
      #   - the first model build with the data of the api
      ###
      def execute_single
        execute[-1]
      end

      ###
      # this method checks whether the api delievered a valid json-response or not
      #
      # * *Args*    :
      #   - +jsonResponse+ -> the plain json received from the api
      #
      # * *Fails* :
      #   - if the response is false, not an array or has a different amount of subresponses then requests
      #   - if there is any subresponse which was not requested
      #   - if there is no model factory mapping for a subresponse
      #
      ###
      def check_response(json_response)
        fail 'UnexpectedResultException!' if
        json_response == false ||
        !json_response.is_a?(Array) ||
        json_response.count != all_query.count

        (0..json_response.count - 1).each do |index|
          current_query = all_query[index]
          response_key  = json_response[index].keys[0]
          query_key     = current_query.keys[0]

          fail 'UnexpectedResultException! result ' + String(query_key) +
            ' expected, but ' + String(response_key) + ' given on position ' +
            String(index) + ' - query: ' + current_query.to_json if
          response_key != query_key

          fail 'UnexpectedResultException! ' + String(response_key) +
            ' is unknown result' unless mapping.key? response_key
        end
      end

      ###
      # parses the plain json result from the api and calls the respective modelfactory methods
      # for building the models for the json resposne
      #
      # * *Args*    :
      #   - +jsonResponse+ -> the plain json received from the api
      #   - +isMultiRequest+ -> boolean which determines whether the request has more then one query or not [optional]
      #
      # * *Returns* :
      #   - an Array containing all models build from the modelfactory
      ###
      def parse_result(json_response, is_multi_request = true)
        check_response(json_response)
        results = []
        query_ids = []

        all_query.each do |query|
          query_ids.push(query.keys[0])
        end

        json_response.each_with_index do |response_object, index|
          current_query   = all_query[index]
          result_key      = response_object.keys[0]
          json_object     = response_object[result_key]
          query_key       = current_query.keys[0]
          factory         = self.factory
          if json_object.is_a?(Hash) && json_object['error_code']
            result = factory.pre_handle_error(
              json_object,
              result_key,
              is_multi_request
            )

            if result != false
              results.push(
                result_key => result
              )
              next
            end
          end

          query    = current_query[query_key]
          query_id = query_ids[index]
          if query_id == QUERY_FACETS
            factory.update_facet_manager(json_object, query)
          elsif query_id == QUERY_TREE
            factory.initialize_category_manager(json_object)
          else
            method = mapping[result_key]
            result = factory.send(method, json_object, query)
            results.push(result)
          end
        end

        results
      end
    end
  end
end
